package com.github.thorqin.toolkit.web.session;

import java.io.IOException;
import java.net.URLDecoder;
import java.security.InvalidKeyException;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.github.thorqin.toolkit.Application;
import com.github.thorqin.toolkit.utility.Encryptor;
import com.github.thorqin.toolkit.web.WebApplication;
import com.google.common.base.Strings;
import org.apache.commons.codec.binary.Base64;
import com.github.thorqin.toolkit.utility.Serializer;

/**
 * This tool class can let developer easy to save session data to client by use cookie storage.
 * since cookie storage have 4k size limit, so we shouldn't save much data to the session.
 * @author nuo.qin
 */
public class ClientSession extends WebSession {

	private final String[] sessionName;
	private final static String keyCode = "kyj1JEkLQ/5To0AF81vlmA==";
	private final static ThreadLocal<Encryptor> encryptorLocalStore = new ThreadLocal<>();
	private boolean isSaved = false;
    private boolean isNew = true;
    private String serverKey = null;
    private String currentSessionName = null;

	public static class Data {
		public String sid;
		public long lastAccessedTime;
		public long creationTime;
		public int maxInterval = 0;
		public Map<String, Object> values = null;
	}
	private Data value;
	
	private static Encryptor getEncryptor(Application application) throws Exception {
		Encryptor obj = encryptorLocalStore.get();
		if (obj == null) {
			obj = Encryptor.createByEncodedKey("aes", Base64.decodeBase64(importKey(application)));
			encryptorLocalStore.set(obj);
		}
		return obj;
	}

	private static Encryptor createEncryptor(String serverKey) throws Exception {
		Encryptor obj = Encryptor.createByEncodedKey("aes", Base64.decodeBase64(serverKey));
		return obj;
	}
	
	private static String importKey(Application application) {
		try {
			if (application != null) {
				return application.readAppTextFile("server.key");
			} else {
				return Serializer.readTextResource("server.key", "utf-8");
			}
		} catch (IOException ex) {
			Logger logger;
			if (application != null)
				logger = application.getLogger();
			else
				logger = Logger.getLogger(ClientSession.class.getName());
			logger.log(Level.WARNING, "server.key does not exists in classes folder, use default key instead.");
			return keyCode;
		}
	}

	private void newSession() {
		this.isSaved = false;
        this.isNew = true;
		value = new Data();
		value.sid = java.util.UUID.randomUUID().toString().replace("-", "");
		value.values = new HashMap<>();
		long now = new Date().getTime();
		value.creationTime = now;
		value.lastAccessedTime = now;
		if (application != null) {
			this.setMaxInactiveInterval(application.getSetting().sessionTimeout);
		} else if (request != null) {
			this.setMaxInactiveInterval(request.getSession().getMaxInactiveInterval());
		} else {
			this.setMaxInactiveInterval(-1);
		}
	}

	private boolean fromCookie(Cookie cookie) throws Exception {
		Encryptor enc = (application == null ? createEncryptor(this.serverKey) : getEncryptor(application));
		String v = cookie.getValue();
		if (Strings.isNullOrEmpty(v)) {
			return false;
		}
		if (v.indexOf("%") >= 0) {
			v = URLDecoder.decode(v, "utf-8");
		}
		byte[] bytes = enc.decrypt(Base64.decodeBase64(v));
		try {
			value = Serializer.fromJsonGzip(bytes, Data.class);
		} catch (Exception e) {
			value = Serializer.fromKryo(bytes);
		}
        if (value == null || isExpired()) {
            return false;
		}
		long now = new Date().getTime();
		if (application != null && application.getSetting().sessionValidTime > 0 && now - value.creationTime > application.getSetting().sessionValidTime * 1000) {
			if (!application.sessionRenew(value, request, response)) {
				this.delete();
				return false;
			} else {
				value.creationTime = now;
			}
		}

		value.lastAccessedTime = now;
        this.isSaved = false;
        this.isNew = false;
        this.currentSessionName = cookie.getName();
        return true;
	}

	public static ClientSession parse(String token, WebApplication application) throws Exception {
		return parse(token, application, null);
	}

	public static ClientSession parse(String token, WebApplication application, String sessionName) throws Exception {
		Encryptor enc = getEncryptor(application);
		if (token == null) {
			throw new InvalidKeyException("Token invalid");
		}
		byte[] bytes = enc.decrypt(Base64.decodeBase64(token));
		ClientSession session = new ClientSession(application, null, null);
		try {
			session.value = Serializer.fromJsonGzip(bytes, Data.class);
		} catch (Exception e) {
			session.value = Serializer.fromKryo(bytes);
		}
		if (session.value == null) {
			throw new InvalidKeyException("Token invalid");
		}
		session.isSaved = false;
		session.isNew = true;
		session.currentSessionName = sessionName;
		return session;
	}

	public static ClientSession parse(String token, String serverKey) throws Exception {
		return parse(token, serverKey, null);
	}

	public static ClientSession parse(String token, String serverKey, String sessionName) throws Exception {
		Encryptor enc = createEncryptor(serverKey);
		if (token == null) {
			throw new InvalidKeyException("Token invalid");
		}
		byte[] bytes = enc.decrypt(Base64.decodeBase64(token));
		ClientSession session = new ClientSession(null, null, null);
		try {
			session.value = Serializer.fromJsonGzip(bytes, Data.class);
		} catch (Exception e) {
			session.value = Serializer.fromKryo(bytes);
		}
		if (session.value == null) {
			throw new InvalidKeyException("Token invalid");
		}
		session.isSaved = false;
		session.isNew = true;
		session.currentSessionName = sessionName;
		return session;
	}

	public ClientSession(String sessionName, String serverKey, HttpServletRequest request, HttpServletResponse response) {
		super(null, request, response);
		this.sessionName = sessionName.split(",");
		this.serverKey = serverKey;
		Cookie[] cookies = (request == null ? null : request.getCookies());
		if (cookies == null) {
			newSession();
		} else {
			for (int i = 0; i < this.sessionName.length; i++) {
				String sName = this.sessionName[i].trim();
				for (Cookie cookie : cookies) {
					if (cookie.getName().equals(sName)) {
						try {
							if (fromCookie(cookie))
								return;
						} catch (Exception ex) {
							logger.log(Level.SEVERE, "Invalid session: " + cookie.getValue(), ex);
						}
					}
				}
			}
			newSession();
		}
	}

	public ClientSession(WebApplication application, HttpServletRequest request, HttpServletResponse response) {
		super(application, request, response);
        if (application != null) {
            if (application.getSetting().sessionName != null) {
                this.sessionName = application.getSetting().sessionName.split(",");
            } else
                this.sessionName = new String[]{"_Thor_Session"};
        } else
            this.sessionName = new String[]{"_Thor_Session"};
		Cookie[] cookies = request == null ? null : request.getCookies();
		if (cookies == null) {
			newSession();
		} else {
			for (int i = 0; i < this.sessionName.length; i++) {
				String sName = this.sessionName[i].trim();
				for (Cookie cookie : cookies) {
					if (cookie.getName().equals(sName)) {
						try {
							if (fromCookie(cookie))
								return;
						} catch (Exception ex) {
							logger.log(Level.SEVERE, "Invalid session: " + cookie.getValue(), ex);
						}
					}
				}
			}
			newSession();
		}
	}

	@Override
	public String getId() {
		return value.sid;
	}

	@Override
	public void set(String key, Object value) {
		this.value.values.put(key, value);
		touch();
	}

    @Override
    public Map<String, Object> getMap() {
        return value.values;
    }

	@Override
	public Object get(String key) {
		Object findValue = this.value.values.get(key);
		return findValue;
	}

	@Override
	public void remove(String key) {
		this.value.values.remove(key);
		touch();
	}

	@Override
	public void clear() {
		this.value.values.clear();
		touch();
	}

	@Override
	public void touch() {
		value.lastAccessedTime = new Date().getTime();
		this.isSaved = false;
        this.isNew = false;
	}

	@Override
	public boolean isSaved() {
		return this.isSaved;
	}

    @Override
    public boolean isNew() {
        return this.isNew;
    }

    @Override
	public String toString() {
		try {
			Encryptor enc = (this.application == null ? createEncryptor(this.serverKey) : getEncryptor(application));
			return Base64.encodeBase64String(enc.encrypt(Serializer.toJsonGzip(value)));
		} catch (Exception ex) {
			logger.log(Level.SEVERE, "Export session failed", ex);
			return null;
		}
	}

    public void delete() {
        if (application != null) {
            String path;
            if (application.getSetting().sessionPath != null)
                path = application.getSetting().sessionPath;
            else
                path = getRootPath();
            save(path, application.getSetting().sessionDomain, 0, true, false);
        } else
            save(getRootPath(), null, 0, true, false);
    }

	public void save(String path, String domain, Integer maxAge, boolean httpOnly, boolean secureOnly) {
		try {
			touch();
			String sessionContent = toString();
			if (sessionContent == null)
				return;
			String sName = (this.currentSessionName != null ? this.currentSessionName : sessionName[0]);
			Cookie cookie = new Cookie(sName, sessionContent);
			if (maxAge != null)
				cookie.setMaxAge(maxAge);
			if (domain != null && !domain.isEmpty())
				cookie.setDomain(domain);
			if (path != null && !path.isEmpty())
				cookie.setPath(path);
			cookie.setHttpOnly(httpOnly);
			cookie.setSecure(secureOnly);
			cookie.setVersion(1);
			if (response != null)
				response.addCookie(cookie);
			this.isSaved = true;
		} catch (Exception ex) {
			logger.log(Level.SEVERE, "Save client session failed!", ex);
		}
	}
	
	public void save(String path, String domain) {
		save(path, domain, null, true, false);
	}
	
	public void save(String path) {
        save(path, null, null, true, false);
	}
	
	private String getRootPath() {
        String path;
        if (application != null)
            path = application.getRootPath(request);
        else if (request != null)
            path = request.getContextPath();
        else
			path = null;
		if (path == null || path.isEmpty())
			return "/";
		else
			return path;
	}

	@Override
	public void save() {
        if (application != null) {
            Integer maxAge = null;
            if (application.getSetting().sessionTimeout < 0) {
                maxAge = application.getSetting().sessionDays * 60 * 60 * 24;
            } else if (application.getSetting().sessionTimeout > 0) {
                maxAge = application.getSetting().sessionTimeout;
            }
            String path;
            if (application.getSetting().sessionPath != null)
                path = application.getSetting().sessionPath;
            else
                path = getRootPath();
            save(path, application.getSetting().sessionDomain, maxAge, true, false);
        } else {
            save(getRootPath(), null, null, true, false);
        }
	}
	
	@Override
	public long getCreationTime() {
		return value.creationTime;
	}

	@Override
	public long getLastAccessedTime() {
		return value.lastAccessedTime;
	}

	@Override
	public void setMaxInactiveInterval(int interval) {
		value.maxInterval = interval;
	}

	@Override
	public int getMaxInactiveInterval() {
		return value.maxInterval;
	}

	public static class IteratorEnumeration<E> implements Enumeration<E> {
		private final Iterator<E> iterator;
		public IteratorEnumeration(Iterator<E> iterator){
			this.iterator = iterator;
		}
		@Override
		public E nextElement() {
			return iterator.next();
		}
		@Override
		public boolean hasMoreElements() {
			return iterator.hasNext();
		}
	}

	@Override
	public Enumeration<String> getKeys() {
		return new IteratorEnumeration<String>(value.values.keySet().iterator());
	}
}
